﻿using System;
using System.Linq;
using System.Xml;
using Notoric.Model.Collections;
using Notoric.Model.Internal;
using Notoric.Model.Packaging;

namespace Notoric.Model.Elements
{
    /// <summary>
    /// Implements a page model element.
    /// </summary>
    /// <remarks>
    /// A page is a WYSIWYG representation of 1 real page of a printed score (<see cref="ScoreElement"/>).
    /// </remarks>
    public class PageElement : ModelElement, IResizableElement
    {
        #region Constructors

        /// <summary>
        /// Creates a new <see cref="PageElement"/> instance.
        /// </summary>
        /// <param name="packagePart">
        /// <para>
        /// Type: <see cref="XmlPackagePart"/>
        /// </para>
        /// <para>
        /// The package part that owns this element.
        /// </para>
        /// </param>
        /// <exception cref="ArgumentNullException">
        /// The <paramref name="packagePart"/> is <see langword="null"/>.
        /// </exception>
        public PageElement(XmlPackagePart packagePart)
            : base(packagePart)
        {
            sections = new ElementCollection<SectionElement>(this, sectionsElement);
            sections.CollectionChanged += new EventHandler<NotifyCollectionChangedEventArgs>(sections_CollectionChanged);

            Height = ((ScorePart)packagePart.Owner).Score.DefaultPageHeight;
            Width = ((ScorePart)packagePart.Owner).Score.DefaultPageWidth;
            orientationAttribute.Value = PageOrientation.Portrait.ToString();

            borderThickness.SetOwner(borderThicknessElement);
            margin.SetOwner(marginElement);
            Background = new SolidColorBrush(new Color(255, 255, 255, 255));
        }

        /// <summary>
        /// Creates a new <see cref="PageElement"/> instance.
        /// </summary>
        /// <param name="packagePart">
        /// <para>
        /// Type: <see cref="XmlPackagePart"/>
        /// </para>
        /// <para>
        /// The package part that owns this element.
        /// </para>
        /// </param>
        /// <param name="element">
        /// <para>
        /// Type: <see cref="XmlElement"/>
        /// </para>
        /// <para>
        /// An XML element that represents this object.
        /// </para>
        /// </param>
        /// <exception cref="ArgumentNullException">
        /// <paramref name="packagePart"/> or <paramref name="element"/> is <see langword="null"/>.
        /// </exception>
        public PageElement(XmlPackagePart packagePart, XmlElement element)
            : base(packagePart, element)
        {
            backgroundElement = element[PackageVocabulary.BackgroundElementName, PackageVocabulary.PackageNamespaceUri];
            borderBrushElement = element[PackageVocabulary.BorderBrushElementName, PackageVocabulary.PackageNamespaceUri];
            borderThicknessElement = element[PackageVocabulary.BorderThicknessElementName, PackageVocabulary.PackageNamespaceUri];
            marginElement = element[PackageVocabulary.MarginElementName, PackageVocabulary.PackageNamespaceUri];
            sectionsElement = element[PackageVocabulary.SectionsElementName, PackageVocabulary.PackageNamespaceUri];

            heightAttribute = element.Attributes[PackageVocabulary.HeightAttribute];
            orientationAttribute = element.Attributes[PackageVocabulary.OrientationAttribute];
            widthAttribute = element.Attributes[PackageVocabulary.WidthAttribute];

            borderThickness = new Thickness((XmlElement)borderThicknessElement.FirstChild);
            margin = new Thickness((XmlElement)marginElement.FirstChild);

            if (backgroundElement.HasChildNodes)
                background = Brush.Deserialize((XmlElement)backgroundElement.FirstChild);
            if (borderBrushElement.HasChildNodes)
                borderBrush = Brush.Deserialize((XmlElement)borderBrushElement.FirstChild);

            sections = new ElementCollection<SectionElement>(this, sectionsElement);
            sections.CollectionChanged += new EventHandler<NotifyCollectionChangedEventArgs>(sections_CollectionChanged);
        }

        #endregion

        #region ModelElement Members

        /// <inheritdoc/>
        protected override XmlElement CreateObjectElement(XmlDocument document)
        {
            var element = document.CreateElement(
                PackageVocabulary.PackageNamespacePrefix,
                PackageVocabulary.PageElementName,
                PackageVocabulary.PackageNamespaceUri
                );

            backgroundElement = document.CreateElement(
                PackageVocabulary.PackageNamespacePrefix,
                PackageVocabulary.BackgroundElementName,
                PackageVocabulary.PackageNamespaceUri
                );

            borderBrushElement = document.CreateElement(
                PackageVocabulary.PackageNamespacePrefix,
                PackageVocabulary.BorderBrushElementName,
                PackageVocabulary.PackageNamespaceUri
                );

            borderThicknessElement = document.CreateElement(
                PackageVocabulary.PackageNamespacePrefix,
                PackageVocabulary.BorderThicknessElementName,
                PackageVocabulary.PackageNamespaceUri
                );

            heightAttribute = document.CreateAttribute(PackageVocabulary.HeightAttribute);

            marginElement = document.CreateElement(
                PackageVocabulary.PackageNamespacePrefix,
                PackageVocabulary.MarginElementName,
                PackageVocabulary.PackageNamespaceUri
                );

            orientationAttribute = document.CreateAttribute(PackageVocabulary.OrientationAttribute);

            sectionsElement = document.CreateElement(
                PackageVocabulary.PackageNamespacePrefix,
                PackageVocabulary.SectionsElementName,
                PackageVocabulary.PackageNamespaceUri
                );

            widthAttribute = document.CreateAttribute(PackageVocabulary.WidthAttribute);

            element.Attributes.Append(heightAttribute);
            element.Attributes.Append(orientationAttribute);
            element.Attributes.Append(widthAttribute);

            element.AppendChild(backgroundElement);
            element.AppendChild(borderBrushElement);
            element.AppendChild(borderThicknessElement);
            element.AppendChild(marginElement);
            element.AppendChild(sectionsElement);

            return element;
        }

        #endregion

        #region IResizableElement Members

        /// <summary>
        /// Gets or sets the height of the page.
        /// </summary>
        /// <value>
        /// <para>
        /// Type: <see cref="System.Double"/>
        /// </para>
        /// <para>
        /// The height (in centimeters) of the page.
        /// </para>
        /// </value>
        public double Height
        {
            get { return double.Parse(heightAttribute.Value, PackageVocabulary.PackageCulture.NumberFormat); }
            set
            {
                heightAttribute.Value = value.ToString(PackageVocabulary.PackageCulture.NumberFormat);

                RaisePropertyChanged("Height");
            }
        }

        /// <summary>
        /// Gets or sets the width of the page.
        /// </summary>
        /// <value>
        /// The width (in centimeters) of the page.
        /// </value>
        public double Width
        {
            get { return double.Parse(widthAttribute.Value, PackageVocabulary.PackageCulture.NumberFormat); }
            set
            {
                widthAttribute.Value = value.ToString(PackageVocabulary.PackageCulture.NumberFormat);

                RaisePropertyChanged("Width");
            }
        }

        #endregion

        #region Event Handlers

        /// <summary>
        /// Occurs when the <see cref="Sections"/> collections has changed.
        /// </summary>
        /// <remarks>
        /// <b>Operation:</b><br/>
        /// If a <see cref="MusicSectionElement"/> is added or removed, the handler creates or removes
        /// a relationship between this page and the <see cref="MusicSectionElement.Composition"/>.
        /// </remarks>
        /// <param name="sender">Object that has sent the event</param>
        /// <param name="e">Event data</param>
        private void sections_CollectionChanged(object sender, NotifyCollectionChangedEventArgs e)
        {
            switch (e.Action)
            {
                case NotifyCollectionChangedAction.Add:
                    foreach (SectionElement section in e.NewItems)
                    {
                        if (section is MusicSectionElement)
                        {
                            var composition = (section as MusicSectionElement).Composition;
                            composition.Composition.AddReferencingSection(section as MusicSectionElement);
                            break;
                        }
                    }
                    break;
                case NotifyCollectionChangedAction.Remove:
                    foreach (SectionElement section in e.OldItems)
                    {
                        if (section is MusicSectionElement)
                        {
                            var composition = (section as MusicSectionElement).Composition;
                            composition.Composition.RemoveReferencingSection(section as MusicSectionElement);
                        }
                    }
                    break;
                case NotifyCollectionChangedAction.Replace:
                    foreach (SectionElement section in e.OldItems)
                    {
                        if (section is MusicSectionElement)
                        {
                            var composition = (section as MusicSectionElement).Composition;
                            composition.Composition.RemoveReferencingSection(section as MusicSectionElement);
                        }
                    }
                    foreach (SectionElement section in e.NewItems)
                    {
                        if (section is MusicSectionElement)
                        {
                            var composition = (section as MusicSectionElement).Composition;
                            composition.Composition.AddReferencingSection(section as MusicSectionElement);
                            break;
                        }
                    }
                    break;
            }
        }

        #endregion

        #region Internal Methods

        /// <summary>
        /// Attempts to handle an incoming border request.
        /// </summary>
        /// <param name="request">
        /// <para>
        /// Type: <see cref="Internal.PageBorderRequest"/>
        /// </para>
        /// <para>
        /// The request data.
        /// </para>
        /// </param>
        /// <returns>
        /// <para>
        /// Type: <see cref="Boolean"/>
        /// </para>
        /// <para>
        /// <see langword="true"/> if the page handled the request, <see langword="false"/> otherwise.
        /// </para>
        /// </returns>
        /// <exception cref="InvalidOperationException">
        /// The <paramref name="request"/> is <see langword="null"/>.<br/>
        /// -or-<br/>
        /// The request is not recognized.<br/>
        /// -or-<br/>
        /// The request data are not valid.
        /// </exception>
        internal bool HandleBorderRequest(Internal.PageBorderRequest request)
        {
            if (request.Request == Internal.BorderRequest.PROCESS_INNER_REQUEST)
            {
                foreach (SectionElement section in sections)
                {
                    if (section != request.InnerRequest.Sender)
                    {
                        if (section.HandleBorderRequest(request.InnerRequest))
                            return true;
                    }
                }
            }

            return false;
        }

        /// <summary>
        /// Processes an incoming music section border request.
        /// </summary>
        /// <remarks>
        /// The request is either passed to a <see cref="SectionInteriorElement.HandleBorderRequest"/> method
        /// if the logical neighbour is found or packed inside a <see cref="Internal.PageBorderRequest"/>
        /// and passed to the <see cref="ScoreElement.ProcessBorderRequest"/> method
        /// of the owning score.
        /// </remarks>
        /// <param name="request">
        /// <para>
        /// Type: <see cref="Internal.MusicSectionBorderRequest"/>
        /// </para>
        /// <para>
        /// The request data.
        /// </para>
        /// </param>
        internal void ProcessBorderRequest(Internal.SectionBorderRequest request)
        {
            if (request == null)
                throw new InvalidOperationException("Request cannot be null!");

            bool handled = false;

            foreach (SectionElement section in sections)
            {
                if (section != request.Sender)
                {
                    if (section.HandleBorderRequest(request))
                    {
                        handled = true;
                        break;
                    }
                }
            }

            if (!handled)
            {
                var score = (ScorePart)(PackagePart as PagePart).Owner;

                var rq = new Internal.PageBorderRequest(
                        Internal.BorderRequest.PROCESS_INNER_REQUEST,
                        request.RequestDirection,
                        this
                        );
                rq.InnerRequest = request;
                score.Score.ProcessBorderRequest(rq);
            }
        }

        #endregion

        #region Public Methods

        /// <summary>
        /// Creates a new <see cref="ImageSectionElement"/> and adds it to the <see cref="Sections"/> collection.
        /// </summary>
        /// <param name="image">
        /// <para>
        /// Type: <see cref="ResourcePart"/>
        /// </para>
        /// <para>
        /// An image resource that will be presented in the new image section.<br/>
        /// This value may be <see langword="null"/>. Then the image section will be empty.
        /// </para>
        /// </param>
        /// <param name="interiorCreator">
        /// <para>
        /// Type: <see cref="SectionInteriorCreator"/>
        /// </para>
        /// <para>
        /// The method that will create the interior of the new image section.
        /// </para>
        /// </param>
        /// <returns>
        /// <para>
        /// Type: <see cref="ImageSectionElement"/>
        /// </para>
        /// <para>
        /// The created image section.
        /// </para>
        /// </returns>
        /// <exception cref="ArgumentNullException">
        /// <paramref name="interiorCreator"/> is <see langword="null"/>.
        /// </exception>
        public ImageSectionElement CreateImageSection(ResourcePart image, SectionInteriorCreator interiorCreator)
        {
            var section = new ImageSectionElement(PackagePart, this, interiorCreator);
            if (image != null)
                section.ImageFile = image.Uri;
            sections.Add(section);

            return section;
        }

        /// <summary>
        /// Creates a new <see cref="MusicSectionElement"/> and adds it to the <see cref="Sections"/> collection.
        /// </summary>
        /// <param name="presentedComposition">
        /// <para>
        /// Type: <see cref="CompositionPart"/>
        /// </para>
        /// <para>
        /// The composition package part that will be presented in the new music section.
        /// </para>
        /// </param>
        /// <param name="firstMeasure">
        /// <para>
        /// Type: <see cref="MeasureElement"/>
        /// </para>
        /// <para>
        /// The first measure of the <paramref name="presentedComposition"/> that will be presented in the new music section.
        /// </para>
        /// </param>
        /// <param name="lastMeasure">
        /// <para>
        /// Type: <see cref="MeasureElement"/>
        /// </para>
        /// The last measure of the <paramref name="presentedComposition"/> that will be presented in the new music section.
        /// </param>
        /// <param name="interiorCreator">
        /// <para>
        /// Type: <see cref="SectionInteriorCreator"/>
        /// </para>
        /// <para>
        /// The method that will create the interior of the new music section.
        /// </para>
        /// </param>
        /// <returns>
        /// <para>
        /// Type: <see cref="MusicSectionElement"/>
        /// </para>
        /// <para>
        /// The created music section.
        /// </para>
        /// </returns>
        /// <exception cref="ArgumentNullException">
        /// Any of the arguments is <see langword="null"/>.
        /// </exception>
        /// <exception cref="ArgumentException">
        /// The <paramref name="firstMeasure"/> or <paramref name="lastMeasure"/> does not belong to the <paramref name="presentedComposition"/>.<br/>
        /// -or-<br/>
        /// The <paramref name="lastMeasure"/> has a lower number than the <paramref name="firstMeasure"/>.
        /// </exception>
        public MusicSectionElement CreateMusicSection(CompositionPart presentedComposition, MeasureElement firstMeasure, 
            MeasureElement lastMeasure, SectionInteriorCreator interiorCreator)
        {
            var section = new MusicSectionElement(PackagePart, this, interiorCreator, presentedComposition, firstMeasure, lastMeasure);
            sections.Add(section);

            return section;
        }

        /// <summary>
        /// Creates a new text section and adds it to the <see cref="Sections"/> collection.
        /// </summary>
        /// <param name="interiorCreator">
        /// <para>
        /// Type: <see cref="SectionInteriorCreator"/>
        /// </para>
        /// <para>
        /// The method that will create the interior of the section.
        /// </para>
        /// </param>
        /// <returns>
        /// <para>
        /// Type: <see cref="TextSectionElement"/>
        /// </para>
        /// <para>
        /// The created text section.
        /// </para>
        /// </returns>
        /// <exception cref="ArgumentNullException">
        /// The <paramref name="interiorCreator"/> is <see langword="null"/>.
        /// </exception>
        public TextSectionElement CreateTextSection(SectionInteriorCreator interiorCreator)
        {
            var section = new TextSectionElement(PackagePart as PagePart, this, interiorCreator);
            sections.Add(section);

            return section;
        }

        #endregion

        #region Public Properties

        /// <summary>
        /// Gets or sets the background brush.
        /// </summary>
        /// <value>
        /// <para>
        /// Type: <see cref="Brush"/>
        /// </para>
        /// <para>
        /// The brush that will paint the backround of the page.
        /// This value may be <see langword="null"/> if the page does not have the background.
        /// </para>
        /// </value>
        public Brush Background
        {
            get { return background; }
            set
            {
                if (background != null)
                    background.SetOwner(null);
                background = value;
                if (background != null)
                    background.SetOwner(backgroundElement);

                RaisePropertyChanged("Background");
            }
        }

        /// <summary>
        /// Gets or sets the border brush.
        /// </summary>
        /// <value>
        /// <para>
        /// Type: <see cref="Brush"/>
        /// </para>
        /// <para>
        /// The brush that will paint the border of the page.
        /// This value may be <see langword="null"/> if the page does not have the border.
        /// </para>
        /// </value>
        public Brush BorderBrush
        {
            get { return borderBrush; }
            set
            {
                if (borderBrush != null)
                    borderBrush.SetOwner(null);
                borderBrush = value;
                if (borderBrush != null)
                    borderBrush.SetOwner(borderBrushElement);

                RaisePropertyChanged("BorderBrush");
            }
        }

        /// <summary>
        /// Gets or sets the thickness of the page border.
        /// </summary>
        /// <value>
        /// <para>
        /// Type: <see cref="Thickness"/>
        /// </para>
        /// <para>
        /// The thickness (in centimeters) of the page border.
        /// </para>
        /// </value>
        public Thickness BorderThickness
        {
            get { return borderThickness; }
            set
            {
                borderThickness.SetOwner(null);
                borderThickness = value;
                borderThickness.SetOwner(borderThicknessElement);

                RaisePropertyChanged("BorderThickness");
            }
        }

        /// <summary>
        /// Gets or sets the margin of the page.
        /// </summary>
        /// <value>
        /// <para>
        /// Type: <see cref="Thickness"/>
        /// </para>
        /// <para>
        /// The margin (in centimeters) of the page.
        /// </para>
        /// </value>
        public Thickness Margin
        {
            get { return margin; }
            set
            {
                margin.SetOwner(null);
                margin = value;
                margin.SetOwner(marginElement);

                RaisePropertyChanged("Margin");
            }
        }

        /// <summary>
        /// Gets or sets the orientation of the page.
        /// </summary>
        /// <value>
        /// <para>
        /// Type: <see cref="PageOrientation"/>
        /// </para>
        /// <para>
        /// The orientation of the page.
        /// </para>
        /// </value>
        public PageOrientation Orientation
        {
            get { return (PageOrientation)Enum.Parse(typeof(PageOrientation), orientationAttribute.Value); }
            set
            {
                orientationAttribute.Value = value.ToString();

                RaisePropertyChanged("Orientation");
            }
        }

        /// <summary>
        /// Gets the collection of sections at this page.
        /// </summary>
        /// <value>
        /// <para>
        /// Type: <see cref="ElementCollection{T}"/> of <see cref="SectionElement"/>
        /// </para>
        /// <para>
        /// The sections at this page.
        /// </para>
        /// </value>
        public ElementCollection<SectionElement> Sections
        {
            get { return sections; }
        }

        #endregion

        #region Private Fields

        /// <summary>
        /// Holds the value of the <see cref="Background"/> property.
        /// </summary>
        private Brush background;
        /// <summary>
        /// Holds the XML element that holds the <see cref="Background"/> element.
        /// </summary>
        private XmlElement backgroundElement;
        /// <summary>
        /// Holds the value of the <see cref="BorderBrush"/> property.
        /// </summary>
        private Brush borderBrush;
        /// <summary>
        /// Holds the XML element that holds the <see cref="BorderBrush"/> element.
        /// </summary>
        private XmlElement borderBrushElement;
        /// <summary>
        /// Holds the value of the <see cref="BorderThickness"/> property.
        /// </summary>
        private Thickness borderThickness;
        /// <summary>
        /// Holds the XML element that holds the <see cref="BorderThickness"/> element.
        /// </summary>
        private XmlElement borderThicknessElement;
        /// <summary>
        /// Holds the XML attribute that holds the <see cref="Height"/> property value.
        /// </summary>
        private XmlAttribute heightAttribute;
        /// <summary>
        /// Holds the value of the <see cref="Margin"/> property.
        /// </summary>
        private Thickness margin;
        /// <summary>
        /// Holds the XML element that holds the <see cref="Margin"/> element.
        /// </summary>
        private XmlElement marginElement;
        /// <summary>
        /// Holds the XML attribute that holds the <see cref="Orientation"/> property value.
        /// </summary>
        private XmlAttribute orientationAttribute;
        /// <summary>
        /// Holds the <see cref="Sections"/> collection.
        /// </summary>
        private ElementCollection<SectionElement> sections;       
        /// <summary>
        /// Holds the XML element that holds the <see cref="Sections"/> collection element.
        /// </summary>
        private XmlElement sectionsElement;
        /// <summary>
        /// Holds the XML attribute that holds the <see cref="Width"/> property value.
        /// </summary>
        private XmlAttribute widthAttribute;

        #endregion
    }

    /// <summary>
    /// Specifies the orientation of a page.
    /// </summary>
    /// <remarks>
    /// The orientation defines the mapping of the <see cref="PageElement.Height"/>
    /// and <see cref="PageElement.Width"/> properties to the <see cref="PageElement.Height"/>,
    /// <see cref="PageElement.Width"/> properties.
    /// </remarks>
    public enum PageOrientation
    {
        /// <summary>
        /// The page orientation is landscape.<br/>
        /// (PageHeight, PageWidth) maps to (Width, Height).
        /// </summary>
        Landscape,
        /// <summary>
        /// The page orientation is portrait.<br/>
        /// (PageHeight, PageWidth) maps to (Height, Width).
        /// </summary>
        Portrait
    }
}
